/**
 * Voice WebSocket Client
 * 仅负责：建立连接、基础事件分发、自动重连、发送 JSON / 二进制
 * 接口：ws(s)://<BASE>/rolePlay/ws/{chatId}
 */

export type VoiceWsEventMap = {
  open: (ev: Event) => void
  close: (ev: CloseEvent) => void
  error: (ev: Event) => void
  text: (data: string, ev: MessageEvent) => void
  binary: (data: ArrayBuffer, ev: MessageEvent) => void
}

export type VoiceWsListener<K extends keyof VoiceWsEventMap> = VoiceWsEventMap[K]

export interface VoiceWsOptions {
  /** http/https 基地址，默认取 import.meta.env.VITE_APP_BASE_API */
  baseURL?: string
  /** 最大重连次数，默认 6 次 */
  maxRetries?: number
  /** 首次重连延时(ms)，默认 500ms，指数退避 */
  retryDelayMs?: number
  /** 是否在收到 close 1000 正常码时也进行重连，默认 false */
  reconnectOnCleanClose?: boolean
}

export class VoiceWsClient {
  private ws: WebSocket | null = null
  private readonly chatId: string
  private readonly baseURL: string
  private readonly opts: Required<VoiceWsOptions>
  private retries = 0
  private manualClosed = false

  private listeners: { [K in keyof VoiceWsEventMap]: Set<VoiceWsEventMap[K]> } = {
    open: new Set(),
    close: new Set(),
    error: new Set(),
    text: new Set(),
    binary: new Set(),
  }

  constructor(chatId: string, options: VoiceWsOptions = {}) {
    this.chatId = chatId
    const envBase = (import.meta as any).env?.VITE_APP_BASE_API || ''
    const baseURL = options.baseURL ?? envBase
    this.baseURL = baseURL.replace(/\/$/, '')
    this.opts = {
      baseURL: this.baseURL,
      maxRetries: options.maxRetries ?? 6,
      retryDelayMs: options.retryDelayMs ?? 500,
      reconnectOnCleanClose: options.reconnectOnCleanClose ?? false,
    }
  }

  /** 计算 ws url（由 http/https 推导为 ws/wss） */
  private get wsURL(): string {
    let url = this.baseURL
    if (!/^wss?:\/\//i.test(url)) {
      url = url.replace(/^http:\/\//i, 'ws://').replace(/^https:\/\//i, 'wss://')
    }
    return `${url}/rolePlay/ws/v2/${encodeURIComponent(this.chatId)}`
  }

  /** 发起连接（若已连接则忽略） */
  connect(): void {
    if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) return

    this.manualClosed = false
    const ws = new WebSocket(this.wsURL)
    ws.binaryType = 'arraybuffer'

    ws.onopen = (ev) => {
      this.retries = 0
      this.emit('open', ev)
    }

    ws.onmessage = (ev) => {
      if (typeof ev.data === 'string') {
        this.emit('text', ev.data, ev)
      } else if (ev.data instanceof ArrayBuffer) {
        this.emit('binary', ev.data, ev)
      } else if ((ev.data as any)?.arrayBuffer) {
        // 兼容 Blob
        ;(ev.data as Blob).arrayBuffer().then((buf) => this.emit('binary', buf, ev))
      }
    }

    ws.onerror = (ev) => {
      this.emit('error', ev)
    }

    ws.onclose = (ev) => {
      this.emit('close', ev)
      this.ws = null
      // 自动重连
      const clean = ev.code === 1000
      if (this.manualClosed) return
      if (clean && !this.opts.reconnectOnCleanClose) return
      this.scheduleReconnect()
    }

    this.ws = ws
  }

  /** 发送 JSON 文本 */
  sendJSON(obj: unknown): void {
    const str = JSON.stringify(obj)
    if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(str)
  }

  /** 发送二进制帧 */
  sendBinary(buf: ArrayBuffer | Uint8Array): void {
    if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(buf)
  }

  /** 手动关闭（不再重连） */
  close(code?: number, reason?: string): void {
    this.manualClosed = true
    this.ws?.close(code, reason)
    this.ws = null
  }

  /** 注册事件 */
  on<K extends keyof VoiceWsEventMap>(type: K, listener: VoiceWsListener<K>): () => void {
    this.listeners[type].add(listener as any)
    return () => this.listeners[type].delete(listener as any)
  }

  private emit<K extends keyof VoiceWsEventMap>(type: K, ...args: Parameters<VoiceWsEventMap[K]>) {
    this.listeners[type].forEach((fn) => {
      try { (fn as any)(...args) } catch (e) { }
    })
  }

  /** 指数退避重连 */
  private scheduleReconnect() {
    if (this.retries >= this.opts.maxRetries) return
    const delay = Math.min(this.opts.retryDelayMs * Math.pow(2, this.retries), 15_000)
    this.retries += 1
    setTimeout(() => {
      if (this.manualClosed) return
      this.connect()
    }, delay)
  }
}

export default VoiceWsClient


